/*
* Copyright (C) 2016 Google Inc. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Helper class that allows us to rename our app.
* Can't just modify SDLActivity, since the native code depends on that package.
*/
package com.google.fpl.pie_noon;
import android.app.Activity;
import android.app.AlarmManager;
import android.app.AlertDialog;
import android.app.PendingIntent;
import android.app.UiModeManager;
import android.content.ActivityNotFoundException;
import android.content.ComponentName;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.graphics.Point;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.net.Uri;
import android.nfc.NdefMessage;
import android.os.Bundle;
import android.util.Log;
import android.util.TypedValue;
import android.view.Display;
import android.view.InputDevice;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.WindowManager;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import com.google.android.apps.santatracker.util.MeasurementManager;
import com.google.android.gms.analytics.GoogleAnalytics;
import com.google.android.gms.analytics.HitBuilders;
import com.google.android.gms.analytics.Tracker;
import com.google.firebase.analytics.FirebaseAnalytics;
import com.google.vrtoolkit.cardboard.CardboardDeviceParams;
import com.google.vrtoolkit.cardboard.CardboardView;
import com.google.vrtoolkit.cardboard.Eye;
import com.google.vrtoolkit.cardboard.HeadTransform;
import com.google.vrtoolkit.cardboard.ScreenParams;
import com.google.vrtoolkit.cardboard.proto.Phone;
import com.google.vrtoolkit.cardboard.sensors.MagnetSensor;
import com.google.vrtoolkit.cardboard.sensors.NfcSensor;
import org.libsdl.app.SDLActivity;
public class FPLActivity extends SDLActivity implements
MagnetSensor.OnCardboardTriggerListener, NfcSensor.OnCardboardNfcListener {
private final String PROPERTY_ID = "XX-XXXXXXXX-X";
private Tracker tracker = null;
private static final float METERS_PER_INCH = 0.0254f;
// Fields used in order to interact with a Cardboard device
private CardboardView cardboardView;
private MagnetSensor magnetSensor;
private NfcSensor nfcSensor;
private HeadTransform headTransform;
private Eye leftEye;
private Eye rightEye;
private Eye monocularEye;
private Eye leftEyeNoDistortion;
private Eye rightEyeNoDistortion;
// Analytics
private FirebaseAnalytics mAnalyics;
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
tracker = GoogleAnalytics.getInstance(this).newTracker(PROPERTY_ID);
// [ANALYTICS]
mAnalyics = FirebaseAnalytics.getInstance(this);
MeasurementManager.recordScreenView(mAnalyics, getString(R.string.analytics_screen_snowdown));
try {
SensorManager sensorManager = (SensorManager)getSystemService(Context.SENSOR_SERVICE);
boolean useCardboard = sensorManager.getDefaultSensor(Sensor.TYPE_GYROSCOPE) != null &&
sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) != null;
// Instantiate fields used by Cardboard
if (useCardboard) {
cardboardView = new CardboardView(this);
headTransform = new HeadTransform();
leftEye = new Eye(Eye.Type.LEFT);
rightEye = new Eye(Eye.Type.RIGHT);
monocularEye = new Eye(Eye.Type.MONOCULAR);
leftEyeNoDistortion = new Eye(Eye.Type.LEFT);
rightEyeNoDistortion = new Eye(Eye.Type.RIGHT);
magnetSensor = new MagnetSensor(this);
magnetSensor.setOnCardboardTriggerListener(this);
nfcSensor = NfcSensor.getInstance(this);
nfcSensor.addOnCardboardNfcListener(this);
NdefMessage tagContents = nfcSensor.getTagContents();
if (tagContents != null) {
updateCardboardDeviceParams(CardboardDeviceParams.createFromNfcContents(tagContents));
}
}
} catch (Exception e) {
Log.e("SDL", "exception", e);
} catch (Error e) {
Log.e("SDL", "error", e);
}
}
@Override
public void onResume() {
super.onResume();
if (cardboardView != null) {
cardboardView.onResume();
magnetSensor.start();
nfcSensor.onResume(this);
}
}
@Override
public void onPause() {
super.onPause();
if (cardboardView != null) {
cardboardView.onPause();
magnetSensor.stop();
nfcSensor.onPause(this);
}
}
// GPG's GUIs need activity lifecycle events to function properly, but
// they don't have access to them. This code is here to route these events
// back to GPG through our C++ code.
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
nativeOnActivityResult(this, requestCode, resultCode, data);
}
boolean textDialogOpen = false;
int queryResponse = -1;
protected boolean UseImmersiveMode() {
final int BUILD_VERSION_KITCAT = 18;
return android.os.Build.VERSION.SDK_INT >= BUILD_VERSION_KITCAT;
}
@Override
public void onWindowFocusChanged(boolean hasFocus) {
super.onWindowFocusChanged(hasFocus);
if (hasFocus) {
textDialogOpen = false;
}
if (UseImmersiveMode() && hasFocus) {
// We use API 15 as our minimum, and these are the only features we
// use in higher APIs, so we define cloned constants:
final int SYSTEM_UI_FLAG_LAYOUT_STABLE = 256; // API 16
final int SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION = 512; // API 16
final int SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN = 1024; // API 16
final int SYSTEM_UI_FLAG_HIDE_NAVIGATION = 2; // API 14
final int SYSTEM_UI_FLAG_FULLSCREEN = 4; // API 16
final int SYSTEM_UI_FLAG_IMMERSIVE_STICKY = 4096; // API 19
mLayout.setSystemUiVisibility(
SYSTEM_UI_FLAG_LAYOUT_STABLE
| SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| SYSTEM_UI_FLAG_HIDE_NAVIGATION
| SYSTEM_UI_FLAG_FULLSCREEN
| SYSTEM_UI_FLAG_IMMERSIVE_STICKY);
}
}
private class TextDialogRunnable implements Runnable {
Activity activity;
String title;
String text;
boolean html;
public TextDialogRunnable(Activity activity, String title, String text,
boolean html) {
this.activity = activity;
this.title = title;
this.text = text;
this.html = html;
}
private class LinkInterceptingWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return false;
}
}
public void run() {
try {
textDialogOpen = true;
WebView webview = new WebView(activity);
webview.setWebViewClient(new LinkInterceptingWebViewClient());
webview.loadData(text, "text/html", null);
AlertDialog alert = new AlertDialog.Builder(activity, AlertDialog.THEME_DEVICE_DEFAULT_DARK)
.setTitle(title)
.setView(webview)
.setNeutralButton("OK", null)
.create();
alert.show();
} catch (Exception e) {
textDialogOpen = false;
Log.e("SDL", "exception", e);
}
}
}
// A Runnable to display a query dialog, asking the user a yes-or-no question.
// Sets queryResponse in the parent class to 0 or 1 when the user responds.
private class QueryDialogRunnable implements Runnable {
Activity activity;
final String title;
final String question;
final String yes;
final String no;
public QueryDialogRunnable(Activity activity, String title, String question,
String yes, String no) {
this.activity = activity;
this.title = title;
this.question = question;
this.yes = yes;
this.no = no;
}
public void run() {
try {
queryResponse = -1;
textDialogOpen = true;
AlertDialog alert = new AlertDialog.Builder(activity, AlertDialog.THEME_DEVICE_DEFAULT_LIGHT)
.setTitle(title)
.setMessage(question)
.setNegativeButton(no, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,int id) {
queryResponse = 0;
textDialogOpen = false;
}})
.setPositiveButton(yes, new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog,int id) {
queryResponse = 1;
textDialogOpen = false;
}})
.create();
alert.show();
} catch (Exception e) {
textDialogOpen = false;
Log.e("SDL", "exception", e);
}
}
}
// Capture motionevents and keyevents to check for gamepad movement. Any events we catch
// (That look like they were from a gamepad or joystick) get sent to C++ via JNI, where
// they are stored, so C++ can deal with them next time it updates the game state.
@Override
public boolean dispatchGenericMotionEvent(MotionEvent event) {
if ((event.getAction() == MotionEvent.ACTION_MOVE) &&
(event.getSource() & (InputDevice.SOURCE_JOYSTICK | InputDevice.SOURCE_GAMEPAD)) != 0) {
float axisX = event.getAxisValue(MotionEvent.AXIS_X);
float axisY = event.getAxisValue(MotionEvent.AXIS_Y);
float hatX = event.getAxisValue(MotionEvent.AXIS_HAT_X);
float hatY = event.getAxisValue(MotionEvent.AXIS_HAT_Y);
float finalX, finalY;
// Decide which values to send, based on magnitude. Hat values, or analog/axis values?
if (Math.abs(axisX) + Math.abs(axisY) > Math.abs(hatX) + Math.abs(hatY)) {
finalX = axisX;
finalY = axisY;
} else {
finalX = hatX;
finalY = hatY;
}
nativeOnGamepadInput(event.getDeviceId(), event.getAction(),
0, // Control Code is not needed for motionEvents.
finalX, finalY);
}
return super.dispatchGenericMotionEvent(event);
}
@Override
public boolean dispatchKeyEvent(KeyEvent event)
{
if ((event.getSource() &
(InputDevice.SOURCE_JOYSTICK | InputDevice.SOURCE_GAMEPAD)) != 0) {
nativeOnGamepadInput(event.getDeviceId(), event.getAction(),
event.getKeyCode(), 0.0f, 0.0f);
}
int keyCode = event.getKeyCode();
// Disable the volume keys while in a cardboard
if ((keyCode == KeyEvent.KEYCODE_VOLUME_DOWN || keyCode == KeyEvent.KEYCODE_VOLUME_UP) &&
(nfcSensor != null && nfcSensor.isDeviceInCardboard())) {
return true;
}
return super.dispatchKeyEvent(event);
}
public void showTextDialog(String title, String text, boolean html) {
runOnUiThread(new TextDialogRunnable(this, title, text, html));
}
public boolean isTextDialogOpen() {
return textDialogOpen;
}
public void showQueryDialog(String title, String query_text, String yes_text, String no_text)
{
runOnUiThread(new QueryDialogRunnable(this, title, query_text, yes_text, no_text));
}
public int getQueryDialogResponse() {
if (textDialogOpen)
return -1;
else
return queryResponse;
}
public void resetQueryDialogResponse() {
queryResponse = -1;
}
public boolean hasSystemFeature(String featureName) {
return getPackageManager().hasSystemFeature(featureName);
}
public void WritePreference(String key, int value) {
SharedPreferences.Editor ed = getPreferences(Context.MODE_PRIVATE).edit();
ed.putInt(key, value);
ed.commit();
}
public int ReadPreference(String key, int default_value) {
return getPreferences(Context.MODE_PRIVATE).getInt(key, default_value);
}
// TODO: Expose this as the JNI function and delete the separate Len() and
// Get() functions below.
private String[] StringArrayResource(String resource_name) {
try {
Resources res = getResources();
int id = res.getIdentifier(resource_name, "array", getPackageName());
return res.getStringArray(id);
} catch (Exception e) {
Log.e("SDL", "exception", e);
return new String[0];
}
}
public int LenStringArrayResource(String resource_name) {
return StringArrayResource(resource_name).length;
}
public String GetStringArrayResource(String resource_name, int index) {
return StringArrayResource(resource_name)[index];
}
public int DpToPx(int dp) {
// Convert the dps to pixels, based on density scale
return (int)TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP, dp, getResources().getDisplayMetrics());
}
public void SendTrackerEvent(String category, String action) {
tracker.send(new HitBuilders.EventBuilder()
.setCategory(category)
.setAction(action)
.build());
}
public void SendTrackerEvent(String category, String action, String label) {
tracker.send(new HitBuilders.EventBuilder()
.setCategory(category)
.setAction(action)
.setLabel(label)
.build());
}
public void SendTrackerEvent(String category, String action, String label, int value) {
tracker.send(new HitBuilders.EventBuilder()
.setCategory(category)
.setAction(action)
.setLabel(label)
.setValue(value)
.build());
}
public int[] GetLandscapedSize() {
Point size = new Point();
// Immersive mode uses the full screen, so get the real size if using it
if (UseImmersiveMode()) {
getWindowManager().getDefaultDisplay().getRealSize(size);
} else {
getWindowManager().getDefaultDisplay().getSize(size);
}
return new int[] { Math.max(size.x, size.y), Math.min(size.x, size.y) };
}
public void SetHeadMountedDisplayResolution(int width, int height) {
// If hardware scaling is used, the width x height will be less than the
// displays natural resolution, so the PPI (pixels per inch) will also
// be different. So, we use this trick to recalculate the ScreenParam's PPI
// values (which are normally just read from the display).
try {
if (cardboardView == null) return;
Display display = getWindowManager().getDefaultDisplay();
ScreenParams sp = new ScreenParams(display);
Phone.PhoneParams pp = new Phone.PhoneParams();
pp.setXPpi(width / sp.getWidthMeters() * METERS_PER_INCH);
pp.setYPpi(height / sp.getHeightMeters() * METERS_PER_INCH);
sp = ScreenParams.fromProto(display, pp);
sp.setWidth(width);
sp.setHeight(height);
cardboardView.updateScreenParams(sp);
} catch (Exception e) {
Log.e("SDL", "exception", e);
}
}
@Override
public void onCardboardTrigger() {
nativeOnCardboardTrigger();
}
@Override
public void onInsertedIntoCardboard(CardboardDeviceParams cardboardDeviceParams) {
updateCardboardDeviceParams(cardboardDeviceParams);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
nativeSetDeviceInCardboard(true);
}
@Override
public void onRemovedFromCardboard() {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
nativeSetDeviceInCardboard(false);
}
protected void updateCardboardDeviceParams(CardboardDeviceParams newParams) {
if (cardboardView == null) {
cardboardView.updateCardboardDeviceParams(newParams);
}
}
// Returns true if the current device is a TV device, false otherwise.
public boolean IsTvDevice() {
UiModeManager uiModeManager = (UiModeManager)getSystemService(UI_MODE_SERVICE);
return uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_TELEVISION;
}
// Function to access the transforms of the eyes, which includes head tracking
public void GetEyeViews(float[] leftTransform, float[] rightTransform) {
if (cardboardView == null) return;
cardboardView.getCurrentEyeParams(headTransform,
leftEye,
rightEye,
monocularEye,
leftEyeNoDistortion,
rightEyeNoDistortion);
if (leftTransform != null && leftTransform.length >= 16) {
float[] leftView = leftEye.getEyeView();
System.arraycopy(leftView, 0, leftTransform, 0, 16);
}
if (rightTransform != null && rightTransform.length >= 16) {
float[] rightView = rightEye.getEyeView();
System.arraycopy(rightView, 0, rightTransform, 0, 16);
}
}
// Reset the head tracker to the current heading
public void ResetHeadTracker() {
if (cardboardView != null) {
cardboardView.resetHeadTracker();
}
}
public void UndistortTexture(int textureId) {
try {
if (cardboardView != null) {
cardboardView.undistortTexture(textureId);
}
} catch (Exception e) {
Log.e("SDL", "exception", e);
}
}
public boolean IsCardboardSupported() {
return (cardboardView != null);
}
// Launch / install Zooshi in Santa mode.
public void LaunchZooshiSanta() {
try {
// Load this URL, which if Zooshi is installed it should handle.
Intent runZooshi = new Intent(
Intent.ACTION_VIEW,
Uri.parse("http://google.github.io/zooshi/launch/default/santa"));
runZooshi.setComponent(
new ComponentName("com.google.fpl.zooshi",
"com.google.fpl.zooshi.ZooshiActivity"));
startActivity(runZooshi);
}
catch (ActivityNotFoundException e) {
// The link wasn't handled by Zooshi.
// Link to the Zooshi store page instead.
try {
if (this.getClass().getSimpleName().equals("FPLTvActivity")) {
// On Android TV, we don't have a web browser, so we need to go
// straight to Google Play to download Zooshi.
startActivity(new Intent(
Intent.ACTION_VIEW,
Uri.parse("market://details?id=com.google.fpl.zooshi")));
}
else {
// Not on an Android TV, so load our landing page instead.
startActivity(new Intent(
Intent.ACTION_VIEW,
Uri.parse("http://google.github.io/zooshi/launch/default/santa")));
}
}
catch (ActivityNotFoundException e2) {
// If we can't do any of these, something is odd about this device.
// I give up.
}
}
}
public void relaunch() {
Context context = getBaseContext();
Intent restartIntent = context.getPackageManager()
.getLaunchIntentForPackage(context.getPackageName() );
PendingIntent intent = PendingIntent.getActivity(
context, 0,
restartIntent, Intent.FLAG_ACTIVITY_CLEAR_TOP);
AlarmManager manager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
int delay = 1;
manager.set(AlarmManager.RTC, System.currentTimeMillis() + delay, intent);
System.exit(2);
}
// Implemented in C++. (gpg_manager.cpp)
private static native void nativeOnActivityResult(
Activity activity,
int requestCode,
int resultCode,
Intent data);
// Implemented in C++. (input.cpp)
private static native void nativeOnGamepadInput(
int controllerId,
int eventCode,
int controlCode,
float x,
float y);
// Implemented in C++. (input.cpp)
private static native void nativeOnCardboardTrigger();
// Implemented in C++. (input.cpp)
private static native void nativeSetDeviceInCardboard(boolean inCardboard);
}